package com.pentajock.docdb.threads;

/*
 * Liscense: 	GPL http://www.gnu.org/copyleft/gpl.html
 * By: 		A. Tres Finocchiaro
 * Date:	2007
 * Java:	6.0SE
 */

/*
 * For doing some of the more combersome and complicated threading, such as
 * listing all documents, populating all documents, or searching all documents
 */
 
import com.pentajock.docdb.utils.DocumentFileFilter;
import java.util.concurrent.atomic.*;
import java.awt.Component;
import java.awt.Cursor;
import com.jacob.activeX.*;
import com.jacob.com.*;
import com.pentajock.docdb.Main;
import com.pentajock.docdb.database.DBController;
import com.pentajock.docdb.dialogs.Dialogs;
import com.pentajock.docdb.dialogs.ProgressDialog;
import com.pentajock.docdb.gui.StatusBar;
import com.pentajock.docdb.types.User;
import com.pentajock.docdb.lang.Lang;
import com.pentajock.docdb.office.Jacob;
import com.pentajock.docdb.types.DepartmentProperties;
import com.pentajock.docdb.types.Document;
import com.pentajock.docdb.types.WeightedDocument;
import com.pentajock.docdb.utils.Utils;
import javax.swing.*;
//import java.lang.*;
import java.sql.*;
import java.util.*;
import java.io.*;

public class DocumentThreadWorker {
	/*
	 * Thread-safe
	 */
	private AtomicReference<Boolean> isWorking = null;
	//private static String microsoftPropertyList;// = "Title, Subject, Author, Category, Keywords, Comments";
	private static List<String> microsoftBuiltInProperties = null;// = Arrays.asList(microsoftPropertyList.split(", "));
	
	
	/*
	 * Not thread-safe
	 */
	private StatusBar statusBar;
	private DefaultListModel threadListModel;
	private Component busyComponent;
	private ProgressDialog progressDialog;
	private DocumentFileFilter fileFilter;
	/*
	 * Final
	 */
	private final String STILL_POPULATING = Lang.DIALOG.getText("STILL_POPULATING", "Still populating.  Please wait.");
	private final String DOCUMENTS_LISTED = Lang.STATUS_BAR.getText("DOCUMENTS_LISTED", "documents listed");
	private final String DB_DOC_MISMATCH = Lang.DIALOG.getText("DOCUMENT_COUNT_MISMATCH", "Count mismatch while deleting all documents");
	private final String DB_DOCPROP_MISMATCH = Lang.DIALOG.getText("DOCPROP_COUNT_MISMATCH", "Count mismatch while deleting all document properties");
	private final String CONFIRM_DOCUMENTS_CLEAR = Lang.DIALOG.getText("CONFIRM_DOCUMENTS_CLEAR", "Would you like to delete all current documents before continuing?");
	private final String REPOPULATE_ERROR = Lang.DIALOG.getText("REPOPULATE_ERROR", "An error occured while repopulating documents");
	private final String SEARCH_CANCELED = Lang.STATUS_BAR.getText("SEARCH_CANCELED", "search canceled");
	
	/*
	 * Constructors
	 */
	public DocumentThreadWorker(DefaultListModel threadListModel) {
		constructorHelper(threadListModel, null, null);
	}
	
	public DocumentThreadWorker(DefaultListModel threadListModel, Component busyComponent) {
		constructorHelper(threadListModel, busyComponent, null);
	}
	
	public DocumentThreadWorker(DefaultListModel threadListModel, Component busyComponent, StatusBar statusBar) {
		constructorHelper(threadListModel, busyComponent, statusBar);
	}
	
	private void constructorHelper(DefaultListModel threadListModel, Component busyComponent, StatusBar statusBar) {
		this.threadListModel = threadListModel;
		this.busyComponent = busyComponent;
		this.statusBar = statusBar;
		this.isWorking = new AtomicReference<Boolean>(new Boolean(false));
		this.fileFilter = new DocumentFileFilter();
                setMicrosoftPropertyList("Title, Subject, Author, Category, Keywords, Comments");
	}
	
	/*
	 * Shouldn't be needed, but may be needed for other languages
	 */
	public void setMicrosoftPropertyList(String propertyList) {
		if (microsoftBuiltInProperties != null)
			microsoftBuiltInProperties.clear();
		else
			microsoftBuiltInProperties = new ArrayList<String>();
		
		for (String s : propertyList.split(","))
			microsoftBuiltInProperties.add(s.trim());
			
	}
	
	public void setMicrosoftPropertyList(List<String> microsoftBuiltInProperties) {
		this.microsoftBuiltInProperties = microsoftBuiltInProperties;
	}
	
	public List<String> getMicrosoftPropertyList() {
		return microsoftBuiltInProperties;
	}
	
	
	/*
	 * Starts a thread that begins adding documents to a listing.  This listing
	 * is a reference passed in through threadListModel.
	 *
	 * 	threadListModel is updated on an event-safe thread, so this
	 * 	will show results as their being populated
	 *
	 * 	busyComponent is optional. It's the JList, JPanel, JDialog or 
	 *	JButton, that you'd like the mouse cursor to appear busy over. 
	 *	(hourglass)
	 * 
	 * 	statusBar is optional.  It simply updates a status bar counter, 
	 * 	etc on an event-safe thread.  Status bar will become more useful
	 * 	once a progressbar is included.
	 *
	 */
	public void listAllDocumentsThread() throws SQLException {
		checkWorking();
		new Thread(new Runnable() {
			public void run() {
				try {
					setWorking(true);
					showWaitCursorThread(busyComponent, true);
					final ResultSet rs = DBController.getAllDocumentsResultSet();
					addDocumentsToListModelThread(threadListModel, statusBar, rs);
				}
				catch (SQLException e) {	
					Main.printStackTrace(e);
				}
				showWaitCursorThread(busyComponent, false);
				setWorking(false);
			}
		}).start();
	}
	
	private void checkWorking() throws SQLException {
		if (isWorking.get())
			throw new SQLException(STILL_POPULATING);
	}
	
	private void setWorking(boolean b) {
		isWorking.set(new Boolean(b));
	}
	
	private void addDocumentsToListModelThread(final DefaultListModel l, final StatusBar s, final ResultSet r) throws SQLException {
		while(r.next()) {
			final Document d = DBController.constructDocument(r);
			d.setProperties(DBController.getDocumentProperties(d.getDocumentID()));
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					try {
						if (s != null)
							s.setLabelText("" + l.getSize() + " " + DOCUMENTS_LISTED);
						l.addElement(d);
					}
					catch (Exception e) {
						Main.printStackTrace(e);
					}
				}
			});
		}
	}
	
	private void showWaitCursorThread(final Component j, final boolean show) {
		if (j!=null) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					if (show) 
						Main.setCursor(j, Cursor.WAIT_CURSOR);
					else 
						Main.setCursor(j, Cursor.DEFAULT_CURSOR);
				}
			});
		}
	}
	
	/*
	 * Pauses the thread to show an error dialog.  Must invokeAndWait, which
	 * can throw an error.
	 */
	private void showThreadSafeErrorDialog(final Exception e) throws Exception {
		SwingUtilities.invokeAndWait(new Runnable() {
			public void run() {
				Dialogs.showError(e, REPOPULATE_ERROR);
			}
		});
	}
	
	private boolean showThreadSafeCanceledDialog() {
		try {
			SwingUtilities.invokeAndWait(new Runnable() {
				public void run() {
					Dialogs.showError(SEARCH_CANCELED);
				}
			});
		}
		catch (Exception e) {
			Main.printStackTrace(e);
		}
		return false;
	}
	
	public void scanForDocuments(User u, File folder, Component parent, final DefaultListModel l) throws SQLException {
		checkWorking();
		//final ArrayList<Document>
		//progressDialog = new ProgressDialog();
		final Set<WeightedDocument> foundDocs = searchForDocs(u, folder, parent);
		
		//foundDocs.addAll();
		final int yesNoCancel = Dialogs.showYesNoCancel(CONFIRM_DOCUMENTS_CLEAR);
		new Thread(new Runnable() {
			public void run() {
				setWorking(true);	
				try {
					switch(yesNoCancel) {
						case JOptionPane.YES_OPTION:
							clearDocuments(); break;
						case JOptionPane.CANCEL_OPTION:
							throw new Exception("Repopulate canceled by user.");
						case JOptionPane.NO_OPTION:
							break;
						default: break;
					}
					
					for (WeightedDocument d : foundDocs) {
						addDocumentToListModelThread(l, d);
					}
				
					putMaintenanceReminderOnDesktop();
				}
				
				catch (Exception e) {
					try {
						showThreadSafeErrorDialog(e);
					}
					catch (Exception ex) {
						Main.printStackTrace(ex);
					}
				}
				setWorking(false);
			}
		}).start();
	}
	
	
	/*
	 * Searches through docRoot for all files that the database supports.  
	 * Only really needed by DBAdminWindow's repopulate call, and should only
	 * need to be run once.
	 */
	public Set<WeightedDocument> searchForDocs(User u, File folder, Component parent) {
		if (folder == null)
			return null;
		progressDialog = new ProgressDialog(parent);
		progressDialog.setVisible(true);
		
		List<String> activeXCalls = DBController.getActiveXCalls();
		List<String> categories = DBController.getCategories();
		Set<WeightedDocument> allDocs = new TreeSet<WeightedDocument>();
		Map<String, DepartmentProperties> allDeptProperties = new HashMap<String, DepartmentProperties>(); 
                
		//final List<String> extensions;
		final List<String> departments;
		try {
			departments = DBController.getAllDepartments();
			//fileFilter = getDocumentFilter();
			//extensions =  DBController.getExtensions();
			for (String department : departments) {
				allDeptProperties.put(department, DBController.getDepartmentPropertiesFull(department));
			}
		}
		catch (Exception e) {
			Dialogs.showError(e);
			return null;
		}
		
		//try {
		//	for (String department : departments) {
			//	allDeptProperties.put(department, DBController.getDepartmentPropertiesFull(department));
			//}
		//}
		//catch (SQLException e) {
		//	Main.printStackTrace(e);
		//}
		
		Collection<File> allFiles = Utils.listFiles(folder, fileFilter, true);
		
		if (allFiles.size() == 0)
			progressDialog.setVisible(false);
		
		for (File f : allFiles) {
			try {
				if (progressDialog.getValue() == JOptionPane.CANCEL_OPTION) {
					Main.printDebug("Cancel pressed");
					return null;
				}
				else if(progressDialog.getValue() == JOptionPane.OK_OPTION) {
					progressDialog.setVisible(true);
				}
				
				int weight = activeXCalls.indexOf(DBController.getActiveXCall(f));
				Document temp = Utils.createDocument(f, u.getUserName(), "NotReady", -1);
				WeightedDocument d = new WeightedDocument(temp, weight);
				allDocs.add(d);
				progressDialog.decrementCounter();
			}
			catch (Exception e) {
				Main.printStackTrace(e, "Error with document " + f.getName());
			}
		}
		
		//allDocs = iterateThroughDocs();
		getMoreDocumentProperties(allDocs, categories, departments, allDeptProperties);
		return allDocs;
	}
	
	
	
	private void addDocumentToListModelThread(final DefaultListModel l, final Document d) {
		SwingUtilities.invokeLater(new Thread(new Runnable() {
			public void run() {
				l.addElement(d);	
			}
		}));
	}
	
	
	/*
	 * Database maintenance reminder on desktop!
	 */
	private boolean putMaintenanceReminderOnDesktop() {
		try {
			 String src = "README Post-Database Population Steps.txt";
			 String dest = System.getProperty("user.home") + "/Desktop/" + src;
			 Utils.copyFromJar(src,dest);
			 Utils.desktopCall("open", new File(dest));
		}
		catch (Exception e) {}
		return false;
	}
	
	private boolean clearDocuments() throws Exception {
		Main.printDebug("Deleting all properties...&");
		if (DBController.getDocumentPropertyCount() == DBController.removeAllDocumentProperties()) {
			Main.printDebug("success");
			Main.printDebug("Deleting all documents...&");
			if (DBController.getDocumentCount() == DBController.removeAllDocuments()) {
				Main.printDebug("success");
				Main.printDebug("Adding all documents...&");
				return true;
			}
			else {
				throw new SQLException(DB_DOC_MISMATCH);
			}
				
		}
		else {
			throw new SQLException(DB_DOCPROP_MISMATCH);
		}
	}
	
	private boolean getMoreDocumentProperties(Set<WeightedDocument> incompleteDocuments, 
						List<String> categories, 
						List<String> departments, 
						Map<String, DepartmentProperties> allDeptProperties) {
		progressDialog.setMaxValue(incompleteDocuments.size());
		progressDialog.resetCounter();
		int prevWeight = -1;
		int counter = 0;
		
		ActiveXComponent oActiveX = null;
		Dispatch oDocument = null;
		for (WeightedDocument d : incompleteDocuments) {
			try {
				if (progressDialog.getValue() == JOptionPane.CANCEL_OPTION) {
					showThreadSafeCanceledDialog();
					return false;
				}
				else
					progressDialog.setVisible(true);

				if (counter == 0 || d.getWeight() != prevWeight)
					oActiveX = Jacob.initiateJacob(d);
				
				if (oActiveX != null)
					oDocument = Jacob.initiateDoc(oActiveX, d, true, true);
				else
					continue;
				
				/*
				 * Set best properties based on what Microsoft Office (or equivelant) finds
				 */
				
				//Utils.prepareDocumentFromScan(oDocument, d, categories, departments);
				progressDialog.incrementCounter();
				String temp = "";
				// DEBUG OUT (SHOULD BE FIXED)
				Object[][] components = d.getComponents();
				for (int i = 0; i < components[0].length; i++)
					temp+=components[1][i] + ": " + components[0][i] + "\n";
				
				DepartmentProperties properties = allDeptProperties.get(d.getDepartmentName());
				//Utils.prepareDocumentPropertiesFromScan(d, properties);
				prepareDocumentPropertiesFromScan(oDocument, d, properties);
				
				for (String propName : d.getPropertyNames()) {
					temp+=propName + ": " + d.getProperty(propName) + "\n";
				}
				
				progressDialog.addMessage(temp + "\n");
				prevWeight = d.getWeight();
			}
			catch (Exception e) {
				Main.printStackTrace(e);
			}
			Jacob.closeDocument(oDocument);
			counter++;
		}
		Jacob.closeApplication(oActiveX);
		progressDialog.setWaitCursor(false);
		progressDialog.addMessage("\n\n" + Lang.MISC.getText("FINISHED", "Finished") + "\n\n");
		return true;
	}
	
	/*
	 * Does some searching through the document properties to gather any 
	 * extra information, then locks the information into the document.
	 * 
	 * For example, if the Manager was "John Doe" when this document was 
	 * created it may be usefull for reporting, even though its not contigent 
	 * on its addition to the database.
	 * 
	 * I guess you could consider this part "optional data" as far as the db
	 * is concerned, although documents created manually through the program 
	 * should adhere strictly to the properties of it's department.
	 */
	private void prepareDocumentPropertiesFromScan(Object docRef, Document d, DepartmentProperties properties) {
		Dispatch oDocument;
		if (docRef instanceof Dispatch && properties != null)
			oDocument = (Dispatch)docRef;
		else
			return;
		
		for (String s : properties.getPropertyNames()) {
			if (s == null || s.trim().equals(""))
				continue;
			String propertyVal = null;
			boolean supportsBuiltIn = DBController.supportsBuiltInProperties(d.toFile());
			boolean supportsCustom = DBController.supportsCustomProperties(d.toFile());
			boolean isBuiltInProperty = microsoftBuiltInProperties.contains(s);
			if (isBuiltInProperty && supportsBuiltIn)
				propertyVal = Jacob.getBuiltInDocumentProperty(oDocument, s);
			else if (supportsCustom)
				propertyVal = Jacob.getCustomDocumentProperty(oDocument, s);
			else
				continue;
			d.getProperties().setProperty(s, propertyVal);
		}
		d.getProperties().lockProperties();
	}
	
	/*
	 * Get a document filter for recursive searching, based on the supported
	 * extensions in the database
	 */
	/*
        //private FilenameFilter getDocumentFilter() throws SQLException {
		final List<String> cachedExtensions = DBController.getExtensions();
	//	return new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return cachedExtensions.contains(Utils.getExtension(name).toLowerCase()) && !name.startsWith("~");
			}
		};
	}*/
}
